diff --git a/puppetboard/app.py b/puppetboard/app.py index 0ac6db7..141fe5f 100644 --- a/puppetboard/app.py +++ b/puppetboard/app.py @@ -1,313 +1,319 @@ from __future__ import unicode_literals from __future__ import absolute_import import os import logging import collections -import urllib +try: + from urllib import unquote +except ImportError: + from urllib.parse import unquote from datetime import datetime, timedelta +from multiprocessing.dummy import Pool as ThreadPool from flask import ( Flask, render_template, abort, url_for, Response, stream_with_context, redirect, request ) from pypuppetdb import connect from puppetboard.forms import QueryForm from puppetboard.utils import ( get_or_abort, yield_or_stop, ten_reports, jsonprint ) app = Flask(__name__) app.config.from_object('puppetboard.default_settings') app.config.from_envvar('PUPPETBOARD_SETTINGS', silent=True) app.secret_key = os.urandom(24) app.jinja_env.filters['jsonprint'] = jsonprint puppetdb = connect( api_version=3, host=app.config['PUPPETDB_HOST'], port=app.config['PUPPETDB_PORT'], ssl_verify=app.config['PUPPETDB_SSL_VERIFY'], ssl_key=app.config['PUPPETDB_KEY'], ssl_cert=app.config['PUPPETDB_CERT'], timeout=app.config['PUPPETDB_TIMEOUT'],) numeric_level = getattr(logging, app.config['LOGLEVEL'].upper(), None) if not isinstance(numeric_level, int): raise ValueError('Invalid log level: %s' % loglevel) logging.basicConfig(level=numeric_level) log = logging.getLogger(__name__) def stream_template(template_name, **context): app.update_template_context(context) t = app.jinja_env.get_template(template_name) rv = t.stream(context) rv.enable_buffering(5) return rv @app.errorhandler(400) def bad_request(e): return render_template('400.html'), 400 @app.errorhandler(403) def bad_request(e): return render_template('403.html'), 400 @app.errorhandler(404) def not_found(e): return render_template('404.html'), 404 @app.errorhandler(412) def precond_failed(e): """We're slightly abusing 412 to handle missing features depending on the API version.""" return render_template('412.html'), 412 @app.errorhandler(500) def server_error(e): return render_template('500.html'), 500 @app.route('/') def index(): """This view generates the index page and displays a set of metrics and latest reports on nodes fetched from PuppetDB. """ # TODO: Would be great if we could parallelize this somehow, doing these # requests in sequence is rather pointless. prefix = 'com.puppetlabs.puppetdb.query.population' - num_nodes = get_or_abort( - puppetdb.metric, - "{0}{1}".format(prefix, ':type=default,name=num-nodes')) - num_resources = get_or_abort( - puppetdb.metric, - "{0}{1}".format(prefix, ':type=default,name=num-resources')) - avg_resources_node = get_or_abort( - puppetdb.metric, - "{0}{1}".format(prefix, ':type=default,name=avg-resources-per-node')) + pool = ThreadPool() + endpoints = [ + "{0}{1}".format(prefix, ':type=default,name=num-nodes'), + "{0}{1}".format(prefix, ':type=default,name=num-resources'), + "{0}{1}".format(prefix, ':type=default,name=avg-resources-per-node'), + ] + fetched_metrics = pool.map(puppetdb.metric, endpoints) metrics = { - 'num_nodes': num_nodes['Value'], - 'num_resources': num_resources['Value'], - 'avg_resources_node': "{0:10.0f}".format(avg_resources_node['Value']), + 'num_nodes': fetched_metrics[0]['Value'], + 'num_resources': fetched_metrics[1]['Value'], + 'avg_resources_node': "{0:10.0f}".format(fetched_metrics[2]['Value']), } nodes = puppetdb.nodes( unreported=app.config['UNRESPONSIVE_HOURS'], with_status=True) nodes_overview = [] stats = { 'changed': 0, 'unchanged': 0, 'failed': 0, 'unreported': 0, } for node in nodes: if node.status == 'unreported': stats['unreported'] += 1 elif node.status == 'changed': stats['changed'] += 1 elif node.status == 'failed': stats['failed'] += 1 else: stats['unchanged'] += 1 if node.status != 'unchanged': nodes_overview.append(node) return render_template( 'index.html', metrics=metrics, nodes=nodes_overview, stats=stats ) @app.route('/nodes') def nodes(): """Fetch all (active) nodes from PuppetDB and stream a table displaying those nodes. Downside of the streaming aproach is that since we've already sent our headers we can't abort the request if we detect an error. Because of this we'll end up with an empty table instead because of how yield_or_stop works. Once pagination is in place we can change this but we'll need to provide a search feature instead. """ status_arg = request.args.get('status', '') nodelist = puppetdb.nodes( unreported=app.config['UNRESPONSIVE_HOURS'], with_status=True) nodes = [] for node in yield_or_stop(nodelist): if status_arg: if node.status == status_arg: nodes.append(node) else: nodes.append(node) return Response(stream_with_context( stream_template('nodes.html', nodes=nodes))) @app.route('/node/') def node(node_name): """Display a dashboard for a node showing as much data as we have on that node. This includes facts and reports but not Resources as that is too heavy to do within a single request. """ node = get_or_abort(puppetdb.node, node_name) facts = node.facts() reports = ten_reports(node.reports()) return render_template( 'node.html', node=node, facts=yield_or_stop(facts), reports=yield_or_stop(reports)) @app.route('/reports') def reports(): """Doesn't do much yet but is meant to show something like the reports of the last half our, something like that.""" return render_template('reports.html') @app.route('/reports/') def reports_node(node): """Fetches all reports for a node and processes them eventually rendering a table displaying those reports.""" reports = ten_reports(yield_or_stop( puppetdb.reports('["=", "certname", "{0}"]'.format(node)))) return render_template( 'reports_node.html', reports=reports, nodename=node) @app.route('/report/latest/') def report_latest(node_name): """Redirect to the latest report of a given node. This is a workaround as long as PuppetDB can't filter reports for latest-report? field. This feature has been requested: http://projects.puppetlabs.com/issues/21554 """ node = get_or_abort(puppetdb.node, node_name) reports = get_or_abort(puppetdb._query, 'reports', query='["=","certname","{0}"]'.format(node_name), limit=1) if len(reports) > 0: report = reports[0]['hash'] return redirect(url_for('report', node=node_name, report_id=report)) else: abort(404) @app.route('/report//') def report(node, report_id): """Displays a single report including all the events associated with that report and their status. """ reports = puppetdb.reports('["=", "certname", "{0}"]'.format(node)) for report in reports: if report.hash_ == report_id: events = puppetdb.events('["=", "report", "{0}"]'.format( report.hash_)) return render_template( 'report.html', report=report, events=yield_or_stop(events)) else: abort(404) @app.route('/facts') def facts(): """Displays an alphabetical list of all facts currently known to PuppetDB.""" facts_dict = collections.defaultdict(list) facts = get_or_abort(puppetdb.fact_names) for fact in facts: letter = fact[0].upper() letter_list = facts_dict[letter] letter_list.append(fact) facts_dict[letter] = letter_list sorted_facts_dict = sorted(facts_dict.items()) return render_template('facts.html', facts_dict=sorted_facts_dict) @app.route('/fact/') def fact(fact): """Fetches the specific fact from PuppetDB and displays its value per node for which this fact is known.""" # we can only consume the generator once, lists can be doubly consumed # om nom nom localfacts = [f for f in yield_or_stop(puppetdb.facts(name=fact))] return Response(stream_with_context(stream_template( 'fact.html', name=fact, facts=localfacts))) @app.route('/fact//') def fact_value(fact, value): """On asking for fact/value get all nodes with that fact.""" facts = get_or_abort(puppetdb.facts, fact, value) localfacts = [f for f in yield_or_stop(facts)] return render_template( 'fact.html', name=fact, value=value, facts=localfacts) @app.route('/query', methods=('GET', 'POST')) def query(): """Allows to execute raw, user created querries against PuppetDB. This is currently highly experimental and explodes in interesting ways since none of the possible exceptions are being handled just yet. This will return the JSON of the response or a message telling you what whent wrong / why nothing was returned.""" if app.config['ENABLE_QUERY']: form = QueryForm() if form.validate_on_submit(): + if form.query.data[0] == '[': + query = form.query.data + else: + query = '[{0}]'.format(form.query.data) result = get_or_abort( puppetdb._query, form.endpoints.data, - query='[{0}]'.format(form.query.data)) + query=query) return render_template('query.html', form=form, result=result) return render_template('query.html', form=form) else: log.warn('Access to query interface disabled by administrator..') abort(403) @app.route('/metrics') def metrics(): metrics = get_or_abort(puppetdb._query, 'metrics', path='mbeans') - for key, value in metrics.iteritems(): + for key, value in metrics.items(): metrics[key] = value.split('/')[3] return render_template('metrics.html', metrics=sorted(metrics.items())) @app.route('/metric/') def metric(metric): - name = urllib.unquote(metric) + name = unquote(metric) metric = puppetdb.metric(metric) return render_template( 'metric.html', name=name, metric=sorted(metric.items())) diff --git a/puppetboard/static/css/puppetboard.css b/puppetboard/static/css/puppetboard.css index 46d002f..01b8e63 100644 --- a/puppetboard/static/css/puppetboard.css +++ b/puppetboard/static/css/puppetboard.css @@ -1,104 +1,65 @@ body { - padding-top: 60px; -} -th.headerSortUp { - position: relative -} -th.headerSortDown { - position: relative -} -th.header { - position: relative -} -th.header:after { - content: "\f0dc"; - font-family: FontAwesome; - font-style: normal; - font-weight: normal; - text-decoration: inherit; - color: #000; - font-size: 18px; - padding-right: 0.5em; - float:right; -} -th.headerSortUp:after { - content: "\f0de"; - font-family: FontAwesome; - font-style: normal; - font-weight: normal; - text-decoration: inherit; - color: #000; - font-size: 18px; - padding-right: 0.5em; - float:right; -} -th.headerSortDown:after { - content: "\f0dd"; - font-family: FontAwesome; - font-style: normal; - font-weight: normal; - text-decoration: inherit; - color: #000; - font-size: 18px; - padding-right: 0.5em; - float:right; -} -.stat { - margin-bottom: 40px; -} -.navbar .brand:hover { - color: #fff; -} -.table tbody tr.error>td { - background-color: #f2dede; -} -h1.error { - color: rgb(223, 46, 27); -} -h1.success { - color: #18BC9C; -} -h1.noop { - color:#aaa; -} -tr.event { - cursor: pointer; -} -td.message { - padding: 0; - border: 0; - background-color: #FFFFE9; -} -div[id^='message-event'] { - display: none; - padding: 4px 15px 4px 15px; -} -.label-count { - width:25px; - text-align:center; -} -.label-time { - width:73px; - text-align:center; -} -.label-status { - width:100px; - text-align:center; -} -.label-nothing { - background-color:#ddd; - color:#ddd; -} -.label.label-failed { - background-color: rgb(231, 76, 60); -} -.label.label-changed { - background-color: rgb(24, 188, 156); -} -.label.label-unreported { - background-color: rgb(231, 76, 60); - background-color: rgb(129, 145, 146); -} -.btn-lastreport { - width:100px; + margin: 0; + font-family: "Open Sans", sans-serif; +} + +a { + color: #2C3E50; + text-decoration: none; +} + +h1.ui.header.no-margin-bottom { + margin-bottom: 0; +} + +.tablesorter-header-inner { + float: left; +} + +th.tablesorter-headerAsc::after { + content: '\25b4' !important; + float: right; +} + +th.tablesorter-headerDesc::after { + content: '\25be' !important; + float: right; +} + +.ui.grid.padding-bottom { + padding-bottom: 40px !important; +} + +.status { + width: 47%; + text-align: center; + display: block; +} +.count { + width: 21%; + text-align: center; + display: block; +} + +.no-margin-top { + margin-top: -35px !important; +} + +.absolute { + position: fixed; + bottom: 0; + width: 100%; + background: #E8E8E8; +} + +.absolute div { + padding: 1em; +} + +.ui.menu.darkblue { + background-color:#2C3E50; +} + +.ui.darkblue.header, i.darkblue { + color:#2C3E50; } diff --git a/puppetboard/static/js/tablesort.min.js b/puppetboard/static/js/tablesort.min.js new file mode 100644 index 0000000..ab17511 --- /dev/null +++ b/puppetboard/static/js/tablesort.min.js @@ -0,0 +1,9 @@ +/* + A simple, lightweight jQuery plugin for creating sortable tables. + https://github.com/kylefox/jquery-tablesort + Version 0.0.2 +*/ +$(function(){var a=window.jQuery;a.tablesort=function(d,c){var e=this;this.$table=d;this.$thead=this.$table.find("thead");this.settings=a.extend({},a.tablesort.defaults,c);this.$table.find("th").bind("click.tablesort",function(){e.sort(a(this))});this.direction=this.$th=this.index=null};a.tablesort.prototype={sort:function(d,c){var e=new Date,b=this,g=this.$table,n=0b.value?1*c:a.value -
-
-

Bad Request

-

The request sent to PuppetDB was invalid. This is usually caused by using an unsupported operator.

-
-
- +{% block content %} +

Bad Request

+

The request sent to PuppetDB was invalid. This is usually caused by using an unsupported operator.

{% endblock %} diff --git a/puppetboard/templates/403.html b/puppetboard/templates/403.html index bfe77d0..a5055d3 100644 --- a/puppetboard/templates/403.html +++ b/puppetboard/templates/403.html @@ -1,11 +1,5 @@ {% extends 'layout.html' %} -{% block row_fluid %} -
-
-
-

Permission Denied

-

What you were looking for has been disabled by the administrator.

-
-
-
+{% block content %} +

Permission Denied

+

What you were looking for has been disabled by the administrator.

{% endblock %} diff --git a/puppetboard/templates/404.html b/puppetboard/templates/404.html index e45a201..e9a99ac 100644 --- a/puppetboard/templates/404.html +++ b/puppetboard/templates/404.html @@ -1,11 +1,5 @@ {% extends 'layout.html' %} -{% block row_fluid %} -
-
-
-

Not Found

-

What you were looking for could not be found in PuppetDB.

-
-
-
+{% block content%} +

Not Found

+

What you were looking for could not be found in PuppetDB.

{% endblock %} diff --git a/puppetboard/templates/500.html b/puppetboard/templates/500.html index b79f451..b35ae0f 100644 --- a/puppetboard/templates/500.html +++ b/puppetboard/templates/500.html @@ -1,16 +1,10 @@ {% extends 'layout.html' %} -{% block row_fluid %} -
-
-
-

Internal Server Error

-

This error usually occurs because: -

    -
  • We were unable to reach PuppetDB;
  • -
  • The query to be executed was malformed resulting in an incorrectly encoded request.
  • -

-

Please have a look at the log output for further information.

-
-
-
+{% block content %} +

Internal Server Error

+

This error usually occurs because: +

    +
  • We were unable to reach PuppetDB;
  • +
  • The query to be executed was malformed resulting in an incorrectly encoded request.
  • +

+

Please have a look at the log output for further information.

{% endblock %} diff --git a/puppetboard/templates/_macros.html b/puppetboard/templates/_macros.html index ba70a69..3c0bad5 100644 --- a/puppetboard/templates/_macros.html +++ b/puppetboard/templates/_macros.html @@ -1,148 +1,148 @@ {% macro facts_table(facts, autofocus=False, condensed=False, show_node=False, show_value=True, link_facts=False, margin_top=20, margin_bottom=20) -%} -
- +
+
- +
{% if show_node %} {% else %} {% endif %} {% if show_value %} {% endif %} {% for fact in facts %} {% if show_node %} {% else %} {% endif %} {% if show_value %} {% endif %} {% endfor %}
NodeFactValue
{{fact.node}}{{fact.name}} {% if link_facts %} {{fact.value}} {% else %} {{fact.value}} {% endif %}
{%- endmacro %} {% macro facts_graph(facts, autofocus=False, condensed=False, show_node=False, margin_top=20, margin_bottom=20) -%} {%- endmacro %} {% macro facts_graph_value(facts, autofocus=False, condensed=False, show_node=False, margin_top=20, margin_bottom=20) -%} {%- endmacro %} {% macro reports_table(reports, nodename, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True) -%} -
+
Only showing the last ten reports.
- +
{% if show_conf_col %} {% endif %} {% if show_agent_col %} {% endif %} {% if show_host_col %} {% endif %} {% for report in reports %} {% if hash_truncate %} - {% set rep_hash = "%s…"|format(report.hash_[0:6])|safe %} + {% set rep_hash = "%s…"|format(report.hash_[0:10])|safe %} {% else %} {% set rep_hash = report.hash_ %} {% endif %} {% if report.failed %} {% else %} {% endif %} {% if show_conf_col %} {% endif %} {% if show_agent_col %} {% endif %} {% if show_host_col %} - + {% endif %} {% endfor %}
Start time Run time Full reportConfiguration versionAgent versionHostname
{{report.start}} {{report.run_time}} {{rep_hash}}{{report.version}}{{report.agent_version}}{{nodename}}{{ report.node }}
{%- endmacro %} diff --git a/puppetboard/templates/fact.html b/puppetboard/templates/fact.html index 9950d74..b6a9782 100644 --- a/puppetboard/templates/fact.html +++ b/puppetboard/templates/fact.html @@ -1,12 +1,12 @@ {% extends 'layout.html' %} {% import '_macros.html' as macros %} {% block content %}

{{name}}{% if value %}/{{value}}{% endif %} ({{facts|length}})

-{{macros.facts_graph(facts, autofocus=True, show_node=True, margin_bottom=10)}} -{{macros.facts_graph_value(facts, autofocus=True, show_node=True, margin_bottom=10)}} +{#{{macros.facts_graph(facts, autofocus=True, show_node=True, margin_bottom=10)}}# +{{macros.facts_graph_value(facts, autofocus=True, show_node=True, margin_bottom=10)}}#} {% if value %} {{macros.facts_table(facts, autofocus=True, show_node=True, show_value=False, margin_bottom=10)}} {% else %} {{macros.facts_table(facts, autofocus=True, show_node=True, link_facts=True, margin_bottom=10)}} {% endif %} {% endblock content %} diff --git a/puppetboard/templates/facts.html b/puppetboard/templates/facts.html index 44177ec..98d390c 100644 --- a/puppetboard/templates/facts.html +++ b/puppetboard/templates/facts.html @@ -1,16 +1,16 @@ {% extends 'layout.html' %} {% block content %} -
- +
+
{%- for key,facts_list in facts_dict %} - {{key}} + {{key}}
    {%- for fact in facts_list %}
  • {{fact}}
  • {%- endfor %}
{% endfor %}
{% endblock content %} diff --git a/puppetboard/templates/index.html b/puppetboard/templates/index.html index b144fad..2aac1fd 100644 --- a/puppetboard/templates/index.html +++ b/puppetboard/templates/index.html @@ -1,102 +1,106 @@ {% extends 'layout.html' %} -{% block row_fluid %} -
- -
-
- - -
- -

{{ stats['unreported'] }} - {% if stats['unreported']== 1 %} node {% else %} nodes {% endif %} -

-
- - unreported in the last {{ config.UNRESPONSIVE_HOURS }} hours - -
+{% block content %} +
+ - -
-
-
-

{{metrics['num_nodes']}}

- Population -
-
-

{{metrics['num_resources']}}

- Resources managed -
-
-

{{metrics['avg_resources_node']}}

- Avg. resources/node -
+
+
+

{{metrics['num_nodes']}}

+ Population +
+
+

{{metrics['num_resources']}}

+ Resources managed +
+
+

{{metrics['avg_resources_node']}}

+ Avg. resources/node
- -
-
+
+
+
+
{% if nodes %} -

Nodes status detail ({{nodes|length}})

- - - - - - - - - - {% for node in nodes %} - {% if node.status != 'unchanged' %} - - - - + + + + {% endif %} + {% endfor %} + +
StatusHostname
- - {{node.status}} - - {% if node.status=='unreported'%} - {{ node.unreported_time }} - {% else %} - {% if node.events['failures'] %}{{node.events['failures']}}{% else %}0{% endif%} - {% if node.events['successes'] %}{{node.events['successes']}}{% else %}0{% endif%} - {% endif %} - {{ node.name }} - {% if node.unreported_time != None or node.status != 'unreported' %} - Latest Report +

Nodes status detail ({{nodes|length}})

+ + + + + + + + + + {% for node in nodes %} + {% if node.status != 'unchanged' %} + + - - {% endif %} - {% endfor %} - -
StatusHostname
+ + {{node.status}} + + {% if node.status=='unreported'%} + {{ node.unreported_time }} {% else %} - No Report + {% if node.events['failures'] %}{{node.events['failures']}}{% else %}0{% endif%} + {% if node.events['successes'] %}{{node.events['successes']}}{% else %}0{% endif%} {% endif %} -
+
+ {{ node.name }} + + + + +
{% else %} -

Nodes status detail

-
- Nothing seems to be changing. -
+

Nodes status detail

+
+ Nothing seems to be changing. +
{% endif %}
-{% endblock row_fluid %} +{% endblock content %} diff --git a/puppetboard/templates/layout.html b/puppetboard/templates/layout.html index a49e942..b7697d4 100644 --- a/puppetboard/templates/layout.html +++ b/puppetboard/templates/layout.html @@ -1,67 +1,53 @@ - Puppetᴃoard - - - + Puppetboard + + + + -